// **********************************************************************
// 
// <copyright>
// 
//  BBN Technologies
//  10 Moulton Street
//  Cambridge, MA 02138
//  (617) 873-8000
// 
//  Copyright (C) BBNT Solutions LLC. All rights reserved.
// 
// </copyright>
// **********************************************************************
// 
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/dataAccess/shape/output/ShpOutputStream.java,v $
// $RCSfile: ShpOutputStream.java,v $
// $Revision: 1.7 $
// $Date: 2009/01/21 01:24:42 $
// $Author: dietrick $
// 
// **********************************************************************

package com.bbn.openmap.dataAccess.shape.output;

import java.awt.geom.Point2D;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.logging.Logger;

import com.bbn.openmap.dataAccess.shape.EsriGraphic;
import com.bbn.openmap.dataAccess.shape.EsriGraphicList;
import com.bbn.openmap.dataAccess.shape.EsriPointList;
import com.bbn.openmap.dataAccess.shape.EsriPolygonList;
import com.bbn.openmap.dataAccess.shape.EsriPolylineList;
import com.bbn.openmap.omGraphics.OMGraphic;
import com.bbn.openmap.omGraphics.OMGraphicList;
import com.bbn.openmap.omGraphics.OMPoint;
import com.bbn.openmap.omGraphics.OMPoly;
import com.bbn.openmap.proj.coords.GeoCoordTransformation;
import com.bbn.openmap.proj.coords.LatLonPoint;

/**
 * Writes data to a .shp file
 * 
 * @author Doug Van Auken
 */
public class ShpOutputStream {

   public static Logger logger = Logger.getLogger("com.bbn.openmap.dataAccess.shape.output.ShpOutputStream");
   public final static int ESRI_RECORD_HEADER_LENGTH = 4; // length in 16-bit
                                                          // words
   /**
    * A GeoCoordTransform to use to convert Lat/Lon values in EsriGraphics to
    * projected coordinates.
    */
   protected GeoCoordTransformation transform;

   /**
    * An outputstream that writes primitive data types in little endian or big
    * endian
    */
   private LittleEndianOutputStream _leos = null;

   /**
    * Creates an outputstream to write to
    * 
    * @param os The output stream to write to
    */
   public ShpOutputStream(OutputStream os) {
      BufferedOutputStream bos = new BufferedOutputStream(os);
      _leos = new LittleEndianOutputStream(bos);
   }

   /**
    * Get the transform being used on the coordinates of the EsriGraphics as
    * they are written to the stream.
    * 
    * @return GeoCoordTransform if used, may be null for no transformation.
    */
   public GeoCoordTransformation getTransform() {
      return transform;
   }

   /**
    * Set the GeoCoordTransform for the stream, so that the EsriGraphics will
    * have their coordinates transformed as they are written to the stream. If
    * null, the coordinates will be unchanged.
    * 
    * @param transform
    */
   public void setTransform(GeoCoordTransformation transform) {
      this.transform = transform;
   }

   /**
    * Determine what type of list is given and write it out.
    * 
    * @param list The EsriGraphicList to write
    * @return The index data that is used to create the .shx file
    */
   public int[][] writeGeometry(EsriGraphicList list)
         throws IOException {
      if (list instanceof EsriPolygonList || list instanceof EsriPolylineList) {
         return writePolyGeometry(list);
      } else if (list instanceof EsriPointList) {
         return writePointGeometry(list);
      }
      return null;
   }

   /**
    * Calculates the content length for each record, in terms of words as
    * defined by ESRI documentation. A word is 16 bits, so a double is 4 words
    * and an int is 2 words.
    * 
    * @param list The EsriGraphicList to write
    * @return The index data that is used to create the .shx file
    */
   protected int[][] createPointIndex(OMGraphicList list) {
      int[][] indexData = new int[2][list.size()];

      int pos = 50;
      for (int i = 0; i < list.size(); i++) {
         // OMGraphicList sublist =
         // (OMGraphicList)list.getOMGraphicAt(i);
         int contentLength = 0;
         contentLength += 2; // Shape Type

         OMGraphic graphic = list.getOMGraphicAt(i);
         if (graphic instanceof EsriGraphicList) {
            EsriGraphicList sublist = (EsriGraphicList) graphic;
            contentLength += (4 * 4); // bounding box, 4 doubles
            contentLength += 2; // number of points, 1 int
            contentLength += (sublist.size() * (2 + 4)); // points, 2 doubles
                                                         // each
         } else {
            contentLength += 4; // X
            contentLength += 4; // Y
         }
         indexData[1][i] = contentLength;
         indexData[0][i] = pos;
         pos += contentLength + 4;
      }
      return indexData;
   }

   /**
    * Creates a two dimensional array holding a list of shape content lengths
    * and shape content length offsets, as defined in Esri shape file
    * documentation. This array is used to create the .shx file
    * 
    * @param list The list from which to create the respective array
    * @return The index data that is used to create the .shx file
    */
   protected int[][] createPolyIndex(EsriGraphicList list) {
      double[] data;

      int[][] indexData = new int[2][list.size()];
      int pos = 50;

      for (int i = 0; i < list.size(); i++) {
         int contentLength = 0;
         OMGraphic graphic = (OMGraphic) list.getOMGraphicAt(i);

         contentLength += 2; // Shape Type
         contentLength += 16; // Box
         contentLength += 2; // NumParts
         contentLength += 2; // NumPoints

         if (graphic instanceof OMGraphicList) {
            OMGraphicList sublist = (OMGraphicList) graphic;
            contentLength += sublist.size() * 2; // offsets?

            for (int j = 0; j < sublist.size(); j++) {
               OMPoly poly = (OMPoly) sublist.getOMGraphicAt(j);
               data = poly.getLatLonArray();

               // each value equals 4 words
               contentLength += data.length * 4;
            }
         } else {
            contentLength += 2; // offset?

            // Should be an EsriPolyline
            data = ((OMPoly) graphic).getLatLonArray();
            // each value equals 4 words
            contentLength += data.length * 4;
         }

         indexData[1][i] = contentLength;
         indexData[0][i] = pos;
         pos += contentLength + 4;
      }
      return indexData;
   }

   /**
    * Creates an array whose elements specify at what index a shapes geometry
    * begins
    * 
    * @param contentLengths The array for which to get offsets from
    * @return An array of record offsets
    */
   protected int[] getRecordOffsets(int[] contentLengths) {
      int[] offsets = new int[contentLengths.length];
      int pos = 50;
      for (int i = 0; i < contentLengths.length; i++) {
         offsets[i] = pos;
         pos += contentLengths[i] + 4;
      }
      return offsets;
   }

   /**
    * Creates an array whose elements specifies at what index a parts geometry
    * begins
    * 
    * @param sublist A list of shapes
    * @return An array of part offsets
    */
   protected int[] getPartOffsets(OMGraphicList sublist) {
      int pos = 0;
      int[] offsets = new int[sublist.size()];
      for (int j = 0; j < sublist.size(); j++) {
         OMPoly poly = (OMPoly) sublist.getOMGraphicAt(j);
         double[] data = poly.getLatLonArray();
         offsets[j] = pos / 2;
         pos += data.length;
      }
      return offsets;
   }

   /**
    * Iterates through a list of shapes, summing the points per part to
    * determine the number of points per shape
    * 
    * @param sublist A list of shapes
    * @return The number of points for a given shape
    */
   protected int getPointsPerShape(OMGraphicList sublist) {
      int numPoints = 0;
      for (int i = 0; i < sublist.size(); i++) {
         OMPoly poly = (OMPoly) sublist.getOMGraphicAt(i);
         double[] data = poly.getLatLonArray();
         numPoints += data.length;
      }
      numPoints /= 2;
      return numPoints;
   }

   protected void writeExtents(double[] extents)
         throws IOException {

      if (_leos == null) {
         return;
      }

      if (extents[0] == 90f && extents[1] == 180f && extents[2] == -90f && extents[3] == -180f) {

         // Whoa! not set from defaults correctly!
         // use old, hardcoded way.
         _leos.writeLEDouble(-180.0);
         _leos.writeLEDouble(-90.0);
         _leos.writeLEDouble(180.0);
         _leos.writeLEDouble(90.0);

      } else {
         if (transform == null) {
            _leos.writeLEDouble(extents[1]);
            _leos.writeLEDouble(extents[0]);
            _leos.writeLEDouble(extents[3]);
            _leos.writeLEDouble(extents[2]);
         } else {
            Point2D pnt = transform.forward(extents[0], extents[1]);
            // extents are written out x, y
            _leos.writeLEDouble(pnt.getX());
            _leos.writeLEDouble(pnt.getY());
            pnt = transform.forward(extents[2], extents[3], pnt);
            _leos.writeLEDouble(pnt.getX());
            _leos.writeLEDouble(pnt.getY());
         }
      }
   }

   /**
    * Writes polygon geometry to the class scope LittleEndianInputStream.
    * 
    * @param list The list of geometry objects to save
    * @return A two dimensional array containing shape offsets and content
    *         lengths
    */
   public int[][] writePolyGeometry(EsriGraphicList list)
         throws IOException {

      OMPoly poly;

      _leos.writeInt(9994); // Byte 0 File Code
      _leos.writeInt(0); // Byte 4 Unused
      _leos.writeInt(0); // Byte 8 Unused
      _leos.writeInt(0); // Byte 12 Unused
      _leos.writeInt(0); // Byte 16 Unused
      _leos.writeInt(0); // Byte 20 Unused

      int[][] indexData = createPolyIndex(list);
      int contentLength = 50;

      if (!list.isEmpty()) {
         contentLength = indexData[0][indexData[0].length - 1] + indexData[1][indexData[0].length - 1] + ESRI_RECORD_HEADER_LENGTH;
      }

      _leos.writeInt(contentLength); // Byte 24 File Length
      _leos.writeLEInt(1000); // Byte 28 Version
      _leos.writeLEInt(list.getType()); // Byte 32 Shape Type

      // Writes bounding box.
      double[] extents = list.getExtents();
      writeExtents(extents);

      _leos.writeDouble(0.0); // Byte 68
      _leos.writeDouble(0.0); // Byte 76
      _leos.writeDouble(0.0); // Byte 84
      _leos.writeDouble(0.0); // Byte 92

      // Temporary point used for transformations
      Point2D pnt = new Point2D.Double();

      // Iterate through the list
      for (int i = 0; i < list.size(); i++) {

         OMGraphic graphic = list.getOMGraphicAt(i);

         // Record header
         _leos.writeInt(i + 1); // Record numbers start with 1
         _leos.writeInt(indexData[1][i]);

         // Beginning of Geometry data
         _leos.writeLEInt(list.getType()); // Little endian

         // More stuff needs to be written out for just the OMPoly
         // case...
         // Single part, etc.

         if (graphic instanceof EsriGraphicList) {

            // Assumes that the elements of the top level list are
            // EsriGraphicLists, too. This will probably be
            // changing.
            EsriGraphicList sublist = (EsriGraphicList) graphic;

            // Writes bounding box.
            extents = sublist.getExtents();
            writeExtents(extents);

            // Writes number of parts
            int numParts = sublist.size();
            _leos.writeLEInt(numParts);

            // Write number of points per shape
            int numPoints = getPointsPerShape(sublist);
            _leos.writeLEInt(numPoints);

            // Write the offsets to each part for a given shape
            int[] offsets = getPartOffsets(sublist);

            for (int j = 0; j < offsets.length; j++) {
               _leos.writeLEInt(offsets[j]);
            }

            // Write the geometry for each part
            for (int j = 0; j < sublist.size(); j++) {
               poly = (OMPoly) sublist.getOMGraphicAt(j);
               double[] data = poly.getLatLonArray();
               int n = 0;
               while (n < data.length) {
                  double lat = Math.toDegrees(data[n++]);
                  double lon = Math.toDegrees(data[n++]);
                  if (transform == null) {
                     _leos.writeLEDouble(lon);
                     _leos.writeLEDouble(lat);
                  } else {
                     transform.forward(lat, lon, pnt);
                     _leos.writeLEDouble(pnt.getX());
                     _leos.writeLEDouble(pnt.getY());
                  }
               }
            }
         } else {
            extents = ((EsriGraphic) graphic).getExtents();
            writeExtents(extents);

            // Writes number of parts for shape (1)
            _leos.writeLEInt(1);

            poly = (OMPoly) graphic;
            double[] data = poly.getLatLonArray();

            // Write number of points for shape
            _leos.writeLEInt(data.length / 2);

            // Write the offsets to this shape
            _leos.writeLEInt(0);

            int n = 0;
            while (n < data.length) {
               double lat = Math.toDegrees(data[n++]);
               double lon = Math.toDegrees(data[n++]);
               if (transform == null) {
                  _leos.writeLEDouble(lon);
                  _leos.writeLEDouble(lat);
               } else {
                  transform.forward(lat, lon, pnt);
                  _leos.writeLEDouble(pnt.getX());
                  _leos.writeLEDouble(pnt.getY());
               }
            }
         }
      }
      _leos.flush();
      _leos.close();
      return indexData;
   }

   /**
    * Writes point geometry to the class scope LittleEndianOutputStream.
    * 
    * @param list An EsriGraphicList of points
    * @return A two dimensional array containing shape offsets and content
    *         lengths
    */
   public int[][] writePointGeometry(EsriGraphicList list)
         throws IOException {
      _leos.writeInt(9994); // Big
      _leos.writeInt(0); // Big
      _leos.writeInt(0); // Big
      _leos.writeInt(0); // Big
      _leos.writeInt(0); // Big
      _leos.writeInt(0); // Big

      int[][] indexData = createPointIndex(list);
      int contentLength = 50;

      if (!list.isEmpty()) {
         contentLength = indexData[0][indexData[0].length - 1] + indexData[1][indexData[0].length - 1] + ESRI_RECORD_HEADER_LENGTH;
      }

      _leos.writeInt(contentLength); // Big
      _leos.writeLEInt(1000); // Little
      _leos.writeLEInt(list.getType());

      // Writes bounding box.
      double[] extents = list.getExtents();
      writeExtents(extents);

      _leos.writeDouble(0.0);
      _leos.writeDouble(0.0);
      _leos.writeDouble(0.0);
      _leos.writeDouble(0.0);

      // For coordinate transformations
      Point2D pnt = new Point2D.Double();
      OMPoint point = null;
      for (int i = 0; i < list.size(); i++) {
         OMGraphic graphic = list.get(i);

         // Record header...
         _leos.writeInt(i + 1); // Record numbers start with 1
         _leos.writeInt(indexData[1][i]);

         // Beginning of Geometry data
         _leos.writeLEInt(list.getType());

         if (graphic instanceof OMGraphicList) {
            EsriGraphicList sublist = (EsriGraphicList) graphic;

            // Writes bounding box.
            extents = sublist.getExtents();
            writeExtents(extents);

            // Write number of points per shape
            _leos.writeLEInt(sublist.size());

            // Write the geometry for each part
            for (int j = 0; j < sublist.size(); j++) {
               point = (OMPoint) sublist.getOMGraphicAt(j);
               LatLonPoint pt = new LatLonPoint.Double(point.getLat(), point.getLon());

                    double lat = pt.getY();
                    double lon = pt.getX();

               if (transform == null) {
                  _leos.writeLEDouble(lon);
                  _leos.writeLEDouble(lat);
               } else {
                  transform.forward(lat, lon, pnt);
                  _leos.writeLEDouble(pnt.getX());
                  _leos.writeLEDouble(pnt.getY());
               }
            }
         } else {
            point = (OMPoint) graphic;
            LatLonPoint pt = new LatLonPoint.Double(point.getLat(), point.getLon());
                double lat = pt.getY();
                double lon = pt.getX();

            if (transform == null) {
               _leos.writeLEDouble(lon);
               _leos.writeLEDouble(lat);
            } else {
               transform.forward(lat, lon, pnt);
               _leos.writeLEDouble(pnt.getX());
               _leos.writeLEDouble(pnt.getY());
            }
         }
      }
      _leos.flush();
      _leos.close();

      return indexData;
   }
}